package jblip.util;

import java.io.IOException;
import java.io.InputStream;

public abstract class Base64Encoder {
  private final static char[] chartable;

  static {
    chartable =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
            .toCharArray();
  }

  public static String encode(final byte[] data) {
    int outlen = 4 * ((data.length / 3) + ((data.length % 3 == 0) ? 0 : 1));
    int pad = 3 - (data.length % 3);

    byte[] output = new byte[outlen];

    int outpos = 0;
    for (int i = 0; i < data.length;) {
      byte[] buff = new byte[3];

      buff[0] = data[i++];
      buff[1] = (i < data.length ? data[i++] : 0);
      buff[2] = (i < data.length ? data[i++] : 0);

      output[outpos++] = (byte) ((buff[0] >>> 2) & 0x3F);
      output[outpos++] = (byte) (((buff[0] << 4) & 0x30) | ((buff[1] >>> 4) & 0x0F));
      output[outpos++] = (byte) (((buff[1] << 2) & 0x3C) | ((buff[2] >>> 6) & 0x03));
      output[outpos++] = (byte) (buff[2] & 0x3F);
      
      for (int j = outpos - 4; j < outpos; j++) {
        if (output[j] < 0 || output[j] > 64) {
          System.err.println("Invalid number " + output[outpos]);
        }
      }
    }

    while (pad != 3 && pad-- > 0) {
      output[--outpos] = 64;
    }

    return bytesToString(output);
  }

  public static String encode(final InputStream input) throws IOException {
    final StringBuilder builder = new StringBuilder();

    final byte[] buff = new byte[4];
    while (encodeChunk(input, buff) != 0) {
      builder.append(bytesToString(buff));
    }

    return builder.toString();
  }

  private static int encodeChunk(final InputStream input, final byte[] chunk)
      throws IOException {
    // encodes maximum of 3 bytes from the input stream and returns 4 byte
    // array of values corresponding to base64 symbols

    if (chunk.length % 4 != 0) {
      throw new IllegalArgumentException("Chunk length must be a multiply of 4");
    }

    int total = 0, next = 0, idx = 0, chunk_idx = 0;

    while (total < chunk.length && (next = input.read()) != -1) {
      switch (idx) {
        case 0:
          chunk[chunk_idx * 4 + 0] = (byte) ((next >>> 2) & 0x3F);
          chunk[chunk_idx * 4 + 1] = (byte) ((next << 4) & 0x30);
          break;
        case 1:
          chunk[chunk_idx * 4 + 1] |= (byte) ((next >>> 4) & 0x0F);
          chunk[chunk_idx * 4 + 2] = (byte) ((next << 2) & 0x3C);
          break;
        case 2:
          chunk[chunk_idx * 4 + 2] |= (byte) ((next >>> 6) & 0x03);
          chunk[chunk_idx * 4 + 3] = (byte) (next & 0x3F);
          chunk_idx++;
          total += 4;
      }
      idx = (idx + 1) % 3;
    }

    while (idx != 0) {
      chunk[chunk_idx * 4 + (4 - idx)] = 64;
      idx = (idx + 1) % 3;
      total++;
    }

    for (int i = total; i < chunk.length; i++) {
      chunk[i] = -1;
    }

    return total;
  }

  private static String bytesToString(byte[] data) {
    StringBuilder build = new StringBuilder(data.length);
    for (byte b : data) {
      if (b != -1) {
        try {
        build.append(chartable[b]);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    }
    return build.toString();
  }
}
